﻿using System;
using System.Collections.Generic;
using System.Linq;

namespace SpotifyTechPuzzles.BilateralProjects
{
    /// <summary>
    /// Spec: Hg$\Docs\Bilateral Projects Puzzle - Spotify.htm
    /// </summary>
    public class BarbadosInvitesCalculator
    {
        private readonly string[] _countAndTeamIdPairs;
        private int _teamCount;
        private IEnumerable<Team> _teams;
        private int _minCount;
        private IEnumerable<ISet<int>> _possibleInvitees;
        private ISet<int> _invitees;
        private int _lastInviteeListPermutationNo;
        private const int FRIEND_ID = 1009;
        private const int FIRST_INVITEE_LIST_PERMUTATION_NO = 0;

        public BarbadosInvitesCalculator(params string[] countAndTeamIdPairs)
        {
            if (countAndTeamIdPairs == null)
                throw new ArgumentNullException("countAndTeamIdPairs");

            _countAndTeamIdPairs = countAndTeamIdPairs;

            InitializeTeamCount();
            InitializeTeams();
            InitializeLastInviteeListPermutationNo();
        }

        private void InitializeLastInviteeListPermutationNo()
        {
            _lastInviteeListPermutationNo = 2.Pow(_teamCount) - 1;
        }

        public IEnumerable<string> GetCountAndInvitees()
        {
            if (_countAndTeamIdPairs.IsEmpty())
                return new string[] {};

            CalculatePossibleInvitees();
            CalculateMinCount();
            CalculateInvitees();

            return GetMinCountAndInviteesAsStrings();
        }

        private void CalculatePossibleInvitees()
        {
            _possibleInvitees = FIRST_INVITEE_LIST_PERMUTATION_NO.To(_lastInviteeListPermutationNo)
                                .Select(CreateInviteeSet);
        }

        private IEnumerable<string> GetMinCountAndInviteesAsStrings()
        {
            return _minCount.ToString().ToEnumerable()
                   .Concat(_invitees.OrderByDescending(id => id).AsStrings());
        }

        private void CalculateInvitees()
        {
            _invitees = _possibleInvitees.Any(ContainsFriendAndIsOfMinCount) 
                            ? _possibleInvitees.First(ContainsFriendAndIsOfMinCount) 
                            : _possibleInvitees.First(IsOfMinCount);
        }

        private void CalculateMinCount()
        {
            _minCount = _possibleInvitees.Min(set => set.Count);
        }

        private bool ContainsFriendAndIsOfMinCount(ISet<int> set)
        {
            return IsOfMinCount(set) && set.Contains(FRIEND_ID);
        }

        private bool IsOfMinCount(ISet<int> set)
        {
            return set.Count == _minCount;
        }

        private ISet<int> CreateInviteeSet(int inviteeListPermutationNo)
        {
            return inviteeListPermutationNo.ToPermutationSequence((uint)_teamCount)
                   .Zip(_teams, (flag, team) => flag ? (int)team.SthlmId : (int)team.LondonId)
                   .ToSet();
        }

        private void InitializeTeams()
        {
            _teams = _countAndTeamIdPairs.Skip(1)
                     .Select(ids => ids.Split(' ')
                                    .Select(id => id.ParseAsInt32()))
                     .Select(idSequence => new Team(idSequence));
        }

        private void InitializeTeamCount()
        {
            _teamCount = _countAndTeamIdPairs.FirstOrDefault().ParseAsInt32();
        }

        internal class Team
        {
            internal readonly SthlmId SthlmId;
            internal readonly LondonId LondonId;

            public Team(SthlmId sthlmId, LondonId londonId)
            {
                SthlmId = sthlmId;
                LondonId = londonId;
            }

            public Team(IEnumerable<int> ids) : this(ids.ElementAt(0), ids.ElementAt(1))
            {}
        }

        internal class SthlmId
        {
            private readonly int _id;

            private SthlmId(int id)
            {
                if (id < 1000 || id > 1999)
                    throw new ArgumentOutOfRangeException("id", id.ToString());

                _id = id;
            }

            public static implicit operator SthlmId(int id)
            {
                return new SthlmId(id);
            }

            public static implicit operator int(SthlmId sthlmId)
            {
                return sthlmId._id;
            }
        }

        internal class LondonId
        {
            private readonly int _id;

            private LondonId(int id)
            {
                if (id < 2000 || id > 2999)
                    throw new ArgumentOutOfRangeException("id", id.ToString());

                _id = id;
            }

            public static implicit operator LondonId(int id)
            {
                return new LondonId(id);
            }

            public static implicit operator int(LondonId londonId)
            {
                return londonId._id;
            }
        }
    }

    internal static class BarbadosInviteCalculatorExtensions
    {
        internal static IEnumerable<int> To(this int @this, int to)
        {
            for (var i = @this; i <= to; i++)
                yield return i;
        }

        internal static int Pow(this int @this, int power)
        {
            return (int)Math.Pow(@this, power);
        }

        internal static ISet<int> ToSet(this IEnumerable<int> source)
        {
            return new HashSet<int>(source);
        }

        internal static int ParseAsInt32(this string @this)
        {
            if (@this.IsNullOrEmpty())
                return 0;

            return int.Parse(@this);
        }

        public static bool IsNullOrEmpty(this string @this)
        {
            return string.IsNullOrEmpty(@this);
        }

        internal static IEnumerable<string> ToEnumerable(this string @this)
        {
            return new[] { @this };
        }

        internal static IEnumerable<string> AsStrings(this IEnumerable<int> @this)
        {
            return @this.Select(t => t.ToString());
        }

        internal static IEnumerable<bool> ToPermutationSequence(this int @this, uint totalWidth)
        {
            return @this.ToPaddedBinaryString(totalWidth)
                   .ToCharArray()
                   .Select(c => c == '0' ? false : true);
        }

        internal static string ToPaddedBinaryString(this int @this, uint totalWidth)
        {
            return Convert.ToString(@this, 2).PadLeft((int)totalWidth, '0');
        }

        internal static bool IsEmpty(this IEnumerable<string> @this)
        {
            return @this.Count() == 0;
        }
    }
}